/**
 * 爬取 markdown 教程网站内容
 * @url 官网文档地址 http://markdown.p2hp.com/index.html
 */

let BasePath = "";
const fs = require("fs");
const cheerio = require("cheerio");
const { indexName, crawlerMap } = require("../config");
const TurndownService = require("turndown");
const tpls = require("./handleTpls");
const {
  writeFileSync,
  fetchHtml,
  typeOf,
  getHomeFeatures,
  getCrawlerNames,
} = require("../utils");
const turndownService = new TurndownService({
  headingStyle: "atx", // 标题样式。	setext or atx    默认：setext
  // hr: "", //Any Thematic break    默认：* * *
  bulletListMarker: "-", //-, +, or *    默认：*
  codeBlockStyle: "fenced", //缩进或围起来。 indented or fenced    默认：indented（如果使用fenced围起来，vuepress不识别console代码高亮语言而一直在控制台打印Language does not exist: console）
  // fence: "", //``` or ~~~    默认：```
  // emDelimiter: "", //_ or *    默认：_
  // strongDelimiter: "", //** or __    默认：**
  // linkStyle: "", //内联或引用。  inlined or referenced    默认：inlined
  // linkReferenceStyle: "", //完整, 折叠或快捷方式。 full, collapsed, or shortcut    默认：full
  // preformattedCode: "", //false or true    默认：false
});

// turndownService.keep(["h1","h2","h3","h4","h5","h6","a","img","code","pre"]);
turndownService.remove(["style", "script"]);

/**
 * 获取导航信息（从html文件中爬取）
 * @param data object 或 array。{url: '', selectors: ['','']} 或 [{id: '', href: '', title: '', children: []}]
 * @param localPath string 有导航数据的 html 文件的本地路径，如果没有，则爬取
 * @param writePath string navs.json的路径，如果没有，则爬取
 * @reture [] 返回的导航数据
 */
async function getNavs(data, localPath, writePath) {
  let navs = [];
  const type = typeOf(data);
  if (type === "Object") {
    const { url = "", selectors = [] } = data;
    const html = await getHtmlStr(localPath, url);
    navs = await getNavsFromHtml(html, selectors);
  } else if (type === "Array") {
    navs = data;
  } else {
    console.log("暂不支持此类型：" + type);
    return [];
  }
  writePath && writeFileSync(writePath, JSON.stringify(navs));
  return navs;
}

/**
 * 获取html文件字符串（先从本地获取，若没有，则从网络爬取）
 * @param localPath string 本地路径
 * @param networkPath string 网络路径
 */
async function getHtmlStr(localPath, networkPath = "") {
  const isExist = fs.existsSync(localPath);
  let html = "";
  if (isExist) {
    html = fs.readFileSync(localPath, "utf8");
  } else {
    const isFullPath = networkPath.startsWith("http");
    networkPath = isFullPath ? networkPath : BasePath + networkPath;
    html = await fetchHtml(networkPath, localPath);
  }
  return html;
}

/**
 * 获取nav-item信息
 */
function getNavItem($el, id, children = []) {
  const text = $el.text().replace(/\n +/g, " ").trim();
  const href = $el.attr("href") || $el.find("a")?.attr("href") || ""; //有可能自己是a标签或自己不是a标签
  const words = text.split(" ");
  const isNum = !isNaN(parseInt(words[0]));
  return {
    id,
    href,
    title: words.slice(1).join(" "),
    children,
  };
}

/**
 * 从html页面中提取导航（navs）数据
 * @param html  html格式的字符串
 * @param seltors 多个层级的选择器，例如：[".book-toc h2", "+ol"],
 * @param writePath Json文件的写入地址
 * @return array 导航数据 示例：[{"id":"1","title":"表达式全集","href":"https://tool.oschina.net/uploads/apidocs/jquery/regexp.html","children": []}]。 注href和children仅可存其一
 */
async function getNavsFromHtml(html = "", seltors = []) {
  const $ = cheerio.load(html);
  const [level_1, level_2, level_3] = seltors;
  const chartMap = {
    "+": "next", //'+' 有共同父级元素，且紧邻的下一个元素（只一个）
    " ": "find", //' ' 后代选择器
    "-": "nextUntil", //'-' 相邻兄弟元素，直到遇到下一个指定元素之前
    "~": "", //'~' 相邻兄弟元素
  };
  return await new Promise((resolve, reject) => {
    const navs = [];
    const char = level_2[0];
    const seltor = level_2.slice(1);
    $(level_1).each((ind, ele) => {
      const subNavs = [];
      let subEles = $(ele)[chartMap[char]](seltor);
      if (char !== "-") {
        subEles = subEles.children();
      }
      subEles.each((i, el) => {
        subNavs.push(getNavItem($(el), `${ind + 1}.${i + 1}`));
      });
      navs.push(getNavItem($(ele), `${ind + 1}`, subNavs));
    });
    resolve(navs);
  });
}

/**
 * 获取md文件的名称
 */
function getMdFileName(navItem = {}) {
  const { id, title, href, children = [] } = navItem;
  const num = id.replace("A", "1");
  const tit = title.replace(/:|：/g, " -");
  return `${num}_${tit}`;
}

/**
 * 写入home页面的READEME.md配置文件
 */
function writeHomeConfig(writePath = `docs/${indexName}`, toPath = "/git/") {
  const features = getHomeFeatures();
  let fileStr = `---
home: true
lang: zh-CN #简体中文
heroImage: /assets/imgs/logo.png
actionText: 起步 →
actionLink: ${toPath}  
`;
  if (features.length) {
    fileStr += "features:";
    features.forEach((item, ind) => {
      const { title, details } = item;
      fileStr += `
  - title: ${title}
    details: ${details}`;
    });
  }
  fileStr += `
footer: 蜀ICP备20024629号
---`;
  // footer: MIT Licensed | Copyright © 2023-present Fan Li Chuan
  writeFileSync(writePath, fileStr);
}

/**
 * 处理并返回htmlFile的主体数据html文本
 */
function getHtmlFileMain(id, htmlFile, handleTpl) {
  const { handleOriginHtmlFile, handleMainHtmlFile, selector } = handleTpl;
  htmlFile = handleOriginHtmlFile
    ? handleOriginHtmlFile(htmlFile, id)
    : htmlFile;
  const $ = cheerio.load(htmlFile);
  const mainSeltor = selector || "body";
  let html = $(mainSeltor)?.html() || "";
  html = html
    .replace(/ src="\//g, ` src="${BasePath}\/`)
    .replace(/ href="\//g, ` href="${BasePath}\/`)
    .replace(/&lt;/g, `\<`)
    .replace(/&gt;/g, `\>`)
    .replace(/&amp;/g, `&`);
  return handleMainHtmlFile ? handleMainHtmlFile(html, id) : html;
}

/**
 * 基础页面 md 文件
 */
async function getRootNavMdFile(name, navItem, handleTpl) {
  const { id: pId = "", title: pTitle = "简介", href, children = [] } = navItem;
  let mdFile = `# ${pTitle}\r\n\r\n这是关于${name}的${pTitle}内容`;
  let htmlFile = "";
  if (!children?.length) {
    if (href) {
      const curPath = `src/${name}/html/${pId}.html`;
      htmlFile = await getHtmlStr(curPath, href);
      htmlFile = getHtmlFileMain(pId, htmlFile, handleTpl);
    } else {
      return mdFile;
    }
  } else {
    const htmls = await Promise.all(
      children.map(async (it, i) => {
        const { id, title, href } = it;
        const curPath = `src/${name}/html/${id}.html`;
        let htFile = await getHtmlStr(curPath, href);
        return getHtmlFileMain(id, htFile, handleTpl);
      })
    );
    htmlFile = `<h1>${pId} ${pTitle}</h1>\r\n` + htmls.join("\r\n");
  }
  mdFile = turndownService.turndown(htmlFile);
  mdFile = mdFile.replace(/```console/g, "```"); //vuepress不识别console代码高亮语言而一直在控制台打印Language does not exist: console;
  return mdFile;
}

/**
 * 爬取数据生成md格式的文件（可从本地或网络爬取）
 * @param root string 爬取到的数据要写入的根文件夹，例：src 或 docs
 * @param endInd 根据导航栏的几个导航来生成数据，默认undefined，即所有导航数据，（设置这个参数，主要是为了方便调试）
 */
async function crawler(
  name = "git",
  root = "docs",
  endInd,
  writeHomeMd = true
) {
  const fails = [];
  const crawlerInfo = crawlerMap[name];
  if (!crawlerInfo) {
    throw new Error("不存在此Name类型：" + name);
  }
  let { basePath, handleTpl } = crawlerInfo;
  if (typeOf(handleTpl) === "String") {
    const tplName = handleTpl;
    handleTpl = tpls[handleTpl];
    if (!handleTpl) {
      throw new Error("不存在此模板名称(handleTpl)：" + tplName);
    }
  }
  BasePath = handleTpl.basePath || basePath;
  // console.log(BasePath, "base-path----------------------AAAA");
  const navs = await getNavs(
    crawlerInfo.navs,
    `src/${name}/html/navs.html`,
    `src/${name}/navs.json`
  );
  // console.log(BasePath, "base-path----------------------BBBB");
  const writeDirPath = `${root}/${name}`;
  const isAddMd = root.startsWith("src");
  await Promise.all(
    navs.slice(0, endInd).map(async (item, ind) => {
      const mdFile = await getRootNavMdFile(name, item, handleTpl);
      const fileName = getMdFileName(item);
      writeFileSync(
        `${writeDirPath}/${isAddMd ? "md/" : ""}${fileName}.md`,
        mdFile
      );
      return;
    })
  );
  writeHomeMd && writeHomeConfig();
  if (writeDirPath.startsWith("docs")) {
    const homeNav = crawlerInfo.home || { id: "0", title: "基础" };
    const mdFile = await getRootNavMdFile(name, homeNav, handleTpl);
    writeFileSync(`${writeDirPath}/${indexName}`, mdFile);
  }
  return fails;
}

/**
 * 主函数：接收 node 命令的参数进行爬取数据
 * @param defNum undefined | number 默认 num 的值 。默认值 undefined
 */
async function main(defNum) {
  const [name = "all", root = "docs", num = defNum] = process.argv.slice(2);
  if (name === "all") {
    const names = getCrawlerNames();
    await Promise.all(
      names.map(async (item, ind) => {
        /**
         * 使用setTimeout是为了解决上方异步不按预期正常执行的问题（跟时间循环机制的宏任务、微任务概念相关）
         * 将上面两处的console.log(……----AAAA)和console.log(……----BBBB)注释放开，分别使用和不使用setTimeout，查看不同表现
         */
        setTimeout(() => {
          crawler(item, root, num, ind === names.length - 1);
        });
        return;
      })
    );
  } else {
    await crawler(name, root, num);
  }
}
main();
